package security.service;

import framework.captcha.Captcha;
import framework.config.SecurityConfig;
import framework.crypto.GeneralCrypto;
import framework.security.*;
import framework.security.password.PasswordService;
import framework.security.token.AuthTokenBuilder;
import framework.security.token.AuthTokenInfo;
import framework.utils.RequestUtil;
import framework.utils.ServletUtil;
import lombok.Getter;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.ServletRequestAttributes;
import security.filters.AuthenticationData;
import security.utils.SecurityAuthUtil;
import security.utils.SecurityCookieUtil;
import security.vo.LoginInfo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录服务基础类型
 */
public class LoginServiceDefault implements LoginService {

    @Getter
    private final SecurityContext securityContext;
    @Getter
    private final SecurityConfig securityConfig;
    @Getter
    private final AccountLoader accountLoader;
    @Getter
    private final CaptchaFlagAdmin captchaFlagAdmin;
    @Getter
    private final GeneralCrypto generalCrypto;
    @Getter
    private final PasswordService passwordService;
    @Getter
    private final AccountChecker accountChecker;
    @Getter
    private final AuthTokenBuilder authTokenBuilder;
    @Getter
    private final AuthTokenGenerator authTokenGenerator;
    @Getter
    private final Captcha captcha;
    @Getter
    private final AuthService authService;

    public LoginServiceDefault(
            GeneralCrypto generalCrypto,
            SecurityContext securityContext,
            Captcha captcha,
            CaptchaFlagAdmin captchaFlagAdmin) {
        this.securityContext = securityContext;
        this.securityConfig = securityContext.getSecurityConfig();
        this.accountLoader = securityContext.getAccountLoader();
        this.accountChecker = securityContext.getAccountChecker();
        this.authTokenBuilder = securityContext.getTokenBuilder();
        this.authTokenGenerator = securityContext.getTokenGenerator();
        this.passwordService = securityContext.getPasswordService();
        this.authService = securityContext.getAuthService();
        this.captchaFlagAdmin = captchaFlagAdmin;
        this.generalCrypto = generalCrypto;
        this.captcha = captcha;
    }

    /**
     * 登录前
     *
     * @param loginEntity
     * @param loginInfo
     */
    protected void onLoginBefore(LoginEntity loginEntity, LoginInfo loginInfo) throws AuthException {
    }

    /**
     * 登录失败后的数据处理与记录
     *
     * @param loginEntity
     * @param exception
     */
    protected void onLoginFailed(LoginEntity loginEntity, AuthException exception) {
        if (exception.isNoLog()) {
            // no log
        } else {
            getAccountLoader().loginUnsuccessful(loginEntity.getUsername(), exception.getMessage());
        }
        // 记录登录异常，要求下次登录使用验证码
        if (StringUtils.hasText(loginEntity.getUsername())) {
            getCaptchaFlagAdmin().setFlag(loginEntity.getUsername());
        }
    }

    /**
     * 登录成功后的数据处理与记录
     *
     * @param loginEntity
     * @param loginInfo
     */
    protected void onLoginSuccess(LoginEntity loginEntity, LoginInfo loginInfo) {
        //更新数据库
        getAccountLoader().loginSuccessful(loginEntity.getUsername(), loginInfo.getId());
        //移除可能存在的登录异常验证码要求记录
        getCaptchaFlagAdmin().remove(loginEntity.getUsername());
    }

    /**
     * 登出前的数据处理与记录
     *
     * @param accountId
     */
    protected void onLogoutBefore(Long accountId) throws AuthException {
    }

    /**
     * 登出成功的数据处理与记录
     *
     * @param accountId
     */
    protected void onLogoutSuccess(Long accountId) {
        //更新数据库
        getAccountLoader().logoutSuccessful(accountId);
    }

    /**
     * 登录失败
     *
     * @param accountId
     * @param exception
     */
    protected void onLogoutFailed(Long accountId, AuthException exception) {

    }

    /**
     * 登入
     *
     * @param loginEntity
     * @return
     */
    @Override
    public LoginInfo login(LoginEntity loginEntity) throws AuthException {
        LoginInfo loginInfo = new LoginInfo();
        try {
            this.doLogin(loginEntity, loginInfo);
        } catch (AuthException exception) {
            this.onLoginFailed(loginEntity, exception);
            throw exception;
        } catch (Exception exception) {
            throw new RuntimeException(exception.getMessage(), exception);
        }
        this.onLoginSuccess(loginEntity, loginInfo);
        return loginInfo;
    }

    private void doLogin(LoginEntity loginEntity, LoginInfo loginInfo) throws AuthException {
        this.onLoginBefore(loginEntity, loginInfo);

        // check captcha
        if (getSecurityConfig().getEnableLoginCaptcha()) {
            String captcha = loginEntity.getCaptcha();
            String captchaId = loginEntity.getCaptchaId();

            // is captcha check
            boolean isCheckCaptcha = StringUtils.hasText(captchaId) && StringUtils.hasText(captcha);

            // is need captcha check
            if (!isCheckCaptcha) {
                // need check to have login failed,
                // permit through from no failed
                isCheckCaptcha = getSecurityConfig().getEnableLoginCaptcha() && this.getCaptchaFlagAdmin().hasFlag(loginEntity.getUsername());
            }

            // captcha check
            if (isCheckCaptcha) {
                this.checkCaptcha(captchaId, captcha);
            }
        }

        // password check
        Account account;
        try {
            account = getAccountChecker().authCheck(loginEntity.getUsername(), loginEntity.getPassword(), loginEntity.getPasswordCipher());
        } catch (AuthException exception) {
            if (getSecurityConfig().getEnableLoginCaptcha()) {
                // enable login captcha
                getCaptchaFlagAdmin().setFlag(loginEntity.getUsername());
            }
            throw exception;
        }

        //
        if (account == null) {
            throw new AuthException(AuthException.FAILED, "System error, authentication check successful but no found account");
        }

        // remember Me
        this.validRememberMe(loginEntity);

        // create sid / session id
        this.createSid(account, loginInfo);

        // create token
        this.createToken(loginEntity, loginInfo, account);
        if (!StringUtils.hasText(loginInfo.getToken())) {
            throw new AuthException(AuthException.FAILED, "System error, not createToken");
        }

        // remember Me
        this.doRememberMe(loginEntity, account, loginInfo.getToken());

        // fill login info
        loginInfo.setId(account.getId());
        loginInfo.setName(account.getName());
        loginInfo.setUsername(loginEntity.getUsername());
        loginInfo.setPasswordChanged(account.passwordMustChanged() ? 1 : 0);

        // set security context
        this.fillSecurityContext(account, loginInfo);
    }

    /**
     * 填充授权上下文
     *
     * @param account
     * @param loginInfo
     */
    protected void fillSecurityContext(Account account, LoginInfo loginInfo) {
        SecurityAuthUtil.authorized(new AuthenticationData(account.getId(), loginInfo.getToken(), loginInfo.getSid()));
    }

    /**
     * 登出
     *
     * @return
     */
    @Override
    public void logout() throws AuthException {
        if (getAuthService().isAuthenticated()) {
            Long accountId = getAuthService().getAccountId();
            try {
                this.onLogoutBefore(accountId);
                this.doLogout(accountId);
            } catch (AuthException exception) {
                this.onLogoutFailed(accountId, exception);
                throw exception;
            } catch (Exception exception) {
                throw new RuntimeException(exception.getMessage(), exception);
            }
            this.onLogoutSuccess(accountId);
        }
    }

    /**
     * 登出
     *
     * @param accountId
     */
    private void doLogout(Long accountId) {
    }

    /**
     * 验证码验证
     *
     * @param captchaId
     * @param captcha
     */
    protected void checkCaptcha(String captchaId, String captcha) throws AuthException {
        if (!StringUtils.hasText(captcha)) {
            throw new AuthException(AuthException.CAPTCHA_ERROR, RequestUtil.getMessageDefault("security.captcha.empty", "Please input captcha code"));
        }

        if (!StringUtils.hasText(captchaId)) {
            throw new AuthException(AuthException.REQUEST_INVALID, "Not set captchaId");
        }

        boolean checkSuccess = getCaptcha().check(captchaId, captcha);
        if (checkSuccess) {
            getCaptcha().remove(captchaId);
        } else {
            throw new AuthException(AuthException.CAPTCHA_ERROR, RequestUtil.getMessageDefault("security.captcha.invalid", "Captcha code error"));
        }
    }

    /**
     * 写入TOKEN到Cookie
     *
     * @param loginEntity
     * @param account
     * @param token
     */
    protected void doRememberMe(LoginEntity loginEntity, Account account, String token) throws AuthException {
        if (loginEntity.getCookie() == null) return;
        if (loginEntity.getCookie() != 1) return;

        // 未设置记住选项
        if (loginEntity.getRememberMe() == null) return;
        if (loginEntity.getRememberMe() < 0) return;
        Integer rememberMe = loginEntity.getRememberMe();

        //
        SecurityConfig securityConfig = getSecurityConfig();
        ServletRequestAttributes attributes = ServletUtil.getRequestAttributes();
        if (attributes == null) return;
        HttpServletResponse response = attributes.getResponse();
        if (response == null) return;

        //
        String cookieName = securityConfig.getTokenParamName();
        if (!StringUtils.hasText(cookieName)) return;

        //
        String cookiePath = securityConfig.getCookiePath();
        if (!StringUtils.hasText(cookiePath)) {
            ServletRequestAttributes requestAttributes = ServletUtil.getRequestAttributes();
            if (requestAttributes != null) {
                HttpServletRequest request = requestAttributes.getRequest();
                if (request != null) {
                    if (StringUtils.hasText(request.getContextPath()) && !request.getContextPath().equals("/")) {
                        cookiePath = request.getContextPath();
                    }
                }
            }
        }

        // fix HttpCookie not support SameSite
        // use custom build
        StringBuffer buffer = SecurityCookieUtil.buildCookie(securityConfig, cookieName, token, cookiePath, rememberMe);
        response.addHeader("Set-Cookie", buffer.toString());
    }

    /**
     * check remember me
     *
     * @param loginEntity
     * @return
     */
    protected void validRememberMe(LoginEntity loginEntity) {
        Integer maxRememberMe = securityConfig.getRememberMeMaxSeconds();
        Integer rememberMe = loginEntity.getRememberMe();
        if (rememberMe == null) {
            rememberMe = 0;
        } else if (rememberMe == 0) {
            //
        } else if (maxRememberMe != null && maxRememberMe > 0 && rememberMe > maxRememberMe) {
            rememberMe = maxRememberMe;
        }
        loginEntity.setRememberMe(rememberMe);
    }

    /**
     * token创建
     *
     * @param loginEntity
     * @param loginInfo
     * @param account
     */
    protected void createToken(LoginEntity loginEntity, LoginInfo loginInfo, Account account) throws AuthException {
        Integer expires;
        if (loginEntity.getRememberMe() != null && loginEntity.getRememberMe() > 0) {
            expires = loginEntity.getRememberMe();
        } else {
            expires = getSecurityConfig().getTokenSeconds();
            if (expires == null) {
                throw new AuthException(AuthException.FAILED, "Please config sys.security.token-seconds to application configuration");
            }
        }

        //
        AuthTokenInfo authTokenInfo = getAuthTokenGenerator().newToken(account.getId(), expires, loginInfo.getSid());
        String token = getAuthTokenBuilder().encode(authTokenInfo);

        //
        loginInfo.setToken(token);
        loginInfo.setTokenExpires(expires);
        loginInfo.setTokenExpired(authTokenInfo.getExpireTime());
    }

    /**
     * 创建sid
     */
    protected void createSid(Account account, LoginInfo loginInfo) {
        loginInfo.setSid(getAuthTokenGenerator().newSid(account.getId()));
    }
}
